/*
 * Copyright (C) 2014 luke.
 * 
 * This file is part of Silvie.
 * 
 * Silvie is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Silvie is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Silvie.  If not, see <http://www.gnu.org/licenses/>.
 */

package silvie.resources.sprite;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import silvie.Silvie;
import silvie.gui.Preview;
import silvie.gui.previewers.AbstractPreviewer;
import silvie.gui.previewers.ImagePreviewer;
import silvie.resources.AbstractResource;
import utils.DataReader;

/**
 * AbstractResource specialization for Silver sprite containers (SPR files).
 * 
 * @author luke
 */
public class SpriteContainer extends AbstractResource {
	private static final int SPRITE_PREVIEW_SPACING = 10;
	private static final int SPRITE_TYPE_OFFSET = 8;
	private static final int SPRITE_COUNT_OFFSET = 12;
	private static final int CONTAINER_DECRIPTION_OFFSET = 28;
	private static final int SPRITE_DESCRIPTION_SIZE = 20;
	private static final int TYPE_RLE = 0;
	private static final int TYPE_OFFSET = 1;
	private static final int TYPE_SIMPLE = 2;
	/*
	 * Which SPR file uses which palette seems to be hardcoded in the game
	 * executable, therefore the sizes of the options sprites are put in this
	 * list to recognize them (SLIDER.SPR)
	 */
	private static final List<Integer> OPTIONS_SPRITE_SIZES = Arrays.asList(3147);
	/*
	 * GLASS.SPR caused problems with the implementation so I hardcoded some
	 * values here
	 */
	private static final int GLASS_SIZE = 6524;
	private static final int GLASS_SPRITEDATA_OFFSET = 1852;
	private byte[] spritePalette;
	private int type;
	private int spriteCount;
	private List<AbstractSprite> sprites;
	private boolean useHardcodedOffset;

	/**
	 * SpriteContainer constructor.
	 * 
	 * @param spriteFile the SPR file.
	 */
	public SpriteContainer(File spriteFile) {
		super(spriteFile, false);
		this.spritePalette = getSpritePalette();
		this.type = -1;
		this.spriteCount = 0;
		this.sprites = new ArrayList<AbstractSprite>();
		this.useHardcodedOffset = false;
	}

	/**
	 * Returns the appropriate palette for this sprite container. It is either
	 * the "options" palette or the "fixed" palette.
	 * 
	 * @return the palette for this container.
	 */
	private byte[] getSpritePalette() {
		return OPTIONS_SPRITE_SIZES.contains((int) super.resource.length()) ? Silvie.OPTIONS_PALETTE : Silvie.FIXED_PALETTE;
	}

	@Override
	public void extract() throws Exception {
		super.extract();
		this.useHardcodedOffset = super.resourceBytes.length == GLASS_SIZE;
		readHeader();
		readContainerDescription();
		readSpriteData();
		for (AbstractSprite sprite : this.sprites) {
			sprite.decode();
		}
	}

	/**
	 * Reads the SPR header, which contains the sprite type and count.
	 */
	private void readHeader() {
		this.type = DataReader.readInt(super.resourceBytes, SPRITE_TYPE_OFFSET);
		this.spriteCount = DataReader.readInt(super.resourceBytes, SPRITE_COUNT_OFFSET);
	}

	/**
	 * Reads the SPR "description" section, which contains the sizes and
	 * dimensions of all contained sprites. This method then instantiates
	 * "empty" sprites based on their type and dimensions.
	 */
	private void readContainerDescription() {
		for (int i = 0; i < this.spriteCount; i++) {
			int size = DataReader.readInt(super.resourceBytes, CONTAINER_DECRIPTION_OFFSET + SPRITE_DESCRIPTION_SIZE * i);
			int width = DataReader.readInt(super.resourceBytes, CONTAINER_DECRIPTION_OFFSET + SPRITE_DESCRIPTION_SIZE * i + DataReader.INT_SIZE);
			int height = DataReader.readInt(super.resourceBytes, CONTAINER_DECRIPTION_OFFSET + SPRITE_DESCRIPTION_SIZE * i + DataReader.INT_SIZE * 2);
			AbstractSprite sprite = null;
			switch (this.type) {
			case TYPE_RLE:
				sprite = new RLESprite(this.spritePalette, size, width, height);
				break;
			case TYPE_OFFSET:
				sprite = new OffsetSprite(this.spritePalette, size, width, height);
				break;
			case TYPE_SIMPLE:
				sprite = new SimpleSprite(this.spritePalette, size, width, height);
				break;
			// The type is always one of these 3, no need to add a default case
			}
			this.sprites.add(sprite);
		}
	}

	/**
	 * Reads and sets each sprite's data, which contains the pixel indices (in
	 * encoded form if {@code type == TYPE_RLE}).
	 */
	private void readSpriteData() {
		int currentPosition = this.useHardcodedOffset ? GLASS_SPRITEDATA_OFFSET : CONTAINER_DECRIPTION_OFFSET + SPRITE_DESCRIPTION_SIZE * this.spriteCount;
		for (AbstractSprite sprite : this.sprites) {
			int spriteDataOffset = findSpriteDataOffset(currentPosition, sprite.getSize());
			currentPosition = spriteDataOffset + DataReader.INT_SIZE;
			int spriteSize = sprite.getSize();
			byte[] spriteData = new byte[spriteSize];
			System.arraycopy(super.resourceBytes, currentPosition, spriteData, 0, spriteSize);
			sprite.setSpriteData(spriteData);
		}
	}

	/**
	 * Utility method for locating the beginning of a sprite's data based on its
	 * size (a sprite's data section always starts with 4 bytes for the size).
	 * 
	 * @param startOffset the offset to start searching at.
	 * @param spriteSize the size of the sprite.
	 * @return the offset of the data in the file, or -1 if not found.
	 */
	private int findSpriteDataOffset(int startOffset, int spriteSize) {
		byte[] sizeBytes = DataReader.getIntBytes(spriteSize);
		for (int i = startOffset; i <= super.resourceBytes.length - DataReader.INT_SIZE; i++) {
			if (super.resourceBytes[i] == sizeBytes[0] && super.resourceBytes[i + 1] == sizeBytes[1] && super.resourceBytes[i + 2] == sizeBytes[2] && super.resourceBytes[i + 3] == sizeBytes[3]) {
				return i;
			}
		}
		return -1;
	}

	@Override
	public AbstractPreviewer getPreviewer(Preview preview) {
		int width = 0;
		int height = 0;
		for (AbstractSprite sprite : this.sprites) {
			height = height > sprite.getHeight() ? height : sprite.getHeight();
			width += sprite.getWidth() + SPRITE_PREVIEW_SPACING;
		}
		width += SPRITE_PREVIEW_SPACING;
		Image image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		Graphics graphics = image.getGraphics();
		int offset = SPRITE_PREVIEW_SPACING;
		for (AbstractSprite sprite : this.sprites) {
			graphics.drawImage(sprite.createImage(), offset, 0, null);
			offset += sprite.getWidth() + SPRITE_PREVIEW_SPACING;
		}
		return new ImagePreviewer(preview, image);
	}
}
